home *** CD-ROM | disk | FTP | other *** search
/ HPAVC / HPAVC CD-ROM.iso / pc / FAQSYS18.ZIP / FAQS.DAT / SERPORT.18 / text0002.txt < prev   
Encoding:
Text File  |  1995-12-12  |  37.5 KB  |  1,222 lines

  1. [... continued ...]
  2.    
  3.  
  4. IRQ sharing - can it be done?   (this applies to ISA bus systems only)
  5. -----------------------------
  6.  
  7. Yes and no. Yes, it can be done in principle, and no, it can't be done
  8. by just configuring two ports to use the same interrupt.
  9.  
  10. Let us first consider the hardware involved. PCs have ICUs (interrupt control
  11. units, or PICs - programmable interrupt controllers) of the 8259A type. They
  12. can be programmed to be triggered by a high signal level or a raising edge,
  13. which is already annoying because low level or falling edge would make add-on
  14. card design simpler. But to top this all off, they have internal pull-up
  15. resistors! Which means that if no card is using the interrupt, it is in
  16. the triggered state.
  17.  
  18. How would cards share interrupts? They'd only be allowed to have their
  19. IRQ output in two states: active high or 'floating'. 'Floating' means the line
  20. is not driven at all, neither high nor low, it 'floats'. If all sharers of
  21. an interrupt line in the PC would only drive the line high or let it 'float',
  22. we'd have a simple interrupt sharing scheme (that would allow for even
  23. simpler design if the active state of the line was low) - if there wasn't
  24. this nasty internal pull-up resistor in the 8259A. <sarcasm on> Sadly IBM
  25. didn't provide an external pull-down resistor on the main board of the very
  26. first PC, so later designs could not have one either for compatibility's
  27. sake. <sarcasm off> 1.5kOhms would be a fine value; the 8259A produces 300uA
  28. that have to be sunk below 0.8v (so 2.6kOhms would be enough in theory,
  29. but having some safety margin can't hurt).
  30.  
  31. So how can you have your ports sharing a common interrupt line? There are
  32. two approaches to this, each assuming you're familiar with using a soldering
  33. iron. What you must provide is a logical OR of all interrupt outputs that
  34. drive the line; while this can be done with an OR gate of course, it is far
  35. more practical to use some wired-OR facility. First you'll have to add the
  36. external pull-down resistor, either on the main board (where it really
  37. belongs) or on one of the cards. Use 1.5kOhms for this. Then cut the line
  38. between the card edge connector and the IRQ line driver (LS125) on each and
  39. every card. Do this carefully; if it's a multi-layer card, you'd better cut
  40. the pin of the LS125, or maybe you can just replace a jumper with a diode.
  41. Now solder a diode (1N4148 will do, slow power diodes won't) over the cut
  42. with the cathode (usually marked with a ring, but you'd better check that
  43. thoroughly if there are multiple rings; the 1N4148 normally has a yellow
  44. cathode ring) to the card edge connector. There you are! Now hardware will no
  45. longer be in the way of interrupt sharing. (A 'cleaner' solution would be to
  46. use a LS126 line driver instead of the diode with 'enable' connected to
  47. 'input', but that's only practical with from-scratch designs.)
  48.  
  49. Now let's face the software problems. In theory, interrupt sharing works fine
  50. between different pieces of hardware, but practically this is limited to real
  51. operating systems that do all interrupt processing by themselves; MSDOS
  52. doesn't do that, so it's not a good option for PCs (even Linux users boot DOS
  53. sometimes, if only to play games). Sharing interrupts even between UARTs
  54. becomes problematic if there are several programs involved, eg. the mouse
  55. driver and some comm application; they'd have to know of each other. 'Daisy
  56. chaining' the interrupt (a program 'hooks' the interrupt by placing its
  57. handler's address in the IRQ serivce table and letting the handler call the
  58. address it found in that table at install time when it exits; no interrupt
  59. acknowledging is done by the handlers themselves, just by the stub handler at
  60. the end of the chain) doesn't work because DOS doesn't even provide a stub
  61. interrupt handler! So one of the programs would have to issue EOI (end of
  62. interrupt) to the ICU, but which one? How would it know it's the last one in
  63. the chain? Better forget daisy chaining interrupts under DOS if you want your
  64. programs to work reliably.
  65.  
  66. The situation is much simpler if all UARTs sharing the same interrupt are
  67. used by the same program. This program has to be aware of the sharing
  68. mechanism, but programs that can make use of more than one serial port
  69. (especially libraries) usually are. Now there's only one problem to be
  70. solved: lock-up situations. As I already wrote, the ICUs in the PC are
  71. programmed to use raising edge trigger mode, and you can't change this
  72. without crashing the system. Now consider the following situation. Two
  73. UARTs share one IRQ line. UART #1 raises the line because it needs service;
  74. the service routine is called and detects that UART #1 needs service. Before
  75. it can perform the serivce, UART #2 raises the IRQ, too. Now UART #1 is
  76. serviced, the line should go to the 'low' state but it doesn't because of
  77. the other UART keeping it high; the handler checks the next UART in its
  78. table and sees that UART #2 needs service, too. Now UART #1 receives another
  79. character and keeps the line high while UART #2 is being serviced. How should
  80. the handler know that this has happened? If it just issued EOI and returned,
  81. the IRQ line would never have gone 'low' during the service, so there won't
  82. be any future raising edges to be detected, and thus no more interrupts!
  83.  
  84. What does the service routine do to avoid lock-ups? It has to mask the
  85. interrupt in the ICU; this resets the edge detector. If it unmasks the
  86. interrupt again at the end of the handler and the line is still 'high',
  87. this will trigger the edge detector and the interrupt will be scheduled
  88. again. See the 'known problems' section for a very solid method of handling
  89. interrupts suggested by Richard Clayton.
  90.  
  91. Windows allows for UARTs sharing interrupts; just make sure the COM ports
  92. are configured properly in the system setup.
  93.  
  94. A note to Linux users: Linux is fully capable of sharing interrupts between
  95. serial ports if the hardware problems described above are solved. Using the
  96. same interrupt for several UARTs even reduces CPU load, so it is definitely a
  97. Good Thing as long as there are not too many sharers. Having a well-designed
  98. and kernel-supported multi-port card is even better because these cards
  99. provide a mechanism for the handler to detect which UART has triggered
  100. interrupt without having to look at every single IIR, which reduces overhead
  101. even further.
  102.  
  103.  
  104.  
  105. Programming
  106. ===========
  107.  
  108. Now for the clickety-clickety thing. I hope you're a bit keen in
  109. assembler programming. Programming the UART in high level languages is,
  110. of course, possible, but not at very high rates. I give you several
  111. routines in assembler and C that do the dirty work for you.
  112.  
  113. If you're keen on examples of how to program the UART in high level
  114. languages, even interrupt-driven, you should have a look at some code
  115. I received from Frank Whaley (ftp: "The_Serial_Port.more04") and at
  116. the "Async Routines Library" Scott A. Deming is currently developing
  117. (ftp: "asyam.zip").
  118.  
  119. First thing to do is detect which chip is used. It shouldn't be difficult
  120. to convert this C function into assembler; I'll omit the assembly version.
  121.  
  122. int detect_UART(unsigned baseaddr)
  123. {
  124.    // this function returns 0 if no UART is installed.
  125.    // 1: 8250, 2: 16450 or 8250 with scratch reg., 3: 16550, 4: 16550A
  126.    int x,olddata;
  127.  
  128.    // check if a UART is present anyway
  129.    olddata=inp(baseaddr+4);
  130.    outp(baseaddr+4,0x10);
  131.    if ((inp(baseaddr+6)&0xf0)) return 0;
  132.    outp(baseaddr+4,0x1f);
  133.    if ((inp(baseaddr+6)&0xf0)!=0xf0) return 0;
  134.    outp(baseaddr+4,olddata);
  135.    // next thing to do is look for the scratch register
  136.    olddata=inp(baseaddr+7);
  137.    outp(baseaddr+7,0x55);
  138.    if (inp(baseaddr+7)!=0x55) return 1;
  139.    outp(baseaddr+7,0xAA);
  140.    if (inp(baseaddr+7)!=0xAA) return 1;
  141.    outp(baseaddr+7,olddata); // we don't need to restore it if it's not there
  142.    // then check if there's a FIFO
  143.    outp(baseaddr+2,1);
  144.    x=inp(baseaddr+2);
  145.    // some old-fashioned software relies on this!
  146.    outp(baseaddr+2,0x0);
  147.    if ((x&0x80)==0) return 2;
  148.    if ((x&0x40)==0) return 3;
  149.    return 4;
  150. }
  151.  
  152. If it's not a 16550A, FIFO mode operation won't work, but there's no
  153. problem in switching it on nevertheless as long as no 16550 is used and
  154. your software is aware that there is no TX FIFO available (see below). If
  155. your software doesn't use the FIFOs explicitly, write 0x7 to the FCR and
  156. mask bits 3, 6 & 7 of the IIR. This does not reduce interrupt overhead but
  157. makes transmission more reliable without changing anything for the software.
  158. But remember that the 16550 has a bug with its FIFOs (see hardware section),
  159. so if the function above returns 3, switch the FIFOs off.
  160.  
  161. Mike Surikov has provided me with an altered version of this function that
  162. works correctly with multi-port serial adapters, too. It's available from
  163. the ftp archive mentioned at the beginning. Look for the file
  164. "The_Serial_Port.more03".
  165.  
  166. The prototype of this useful function has also been provided by Mike
  167. Surikov; I've rewritten it from scratch though. It allows you to detect which
  168. interrupt is used by a certain UART. There is an assembly version of Mike's
  169. version (which can only detect intlevels 0-7) of this function as well. It's
  170. available from the ftp archive as "The_Serial_Port.more02".
  171.  
  172. int detect_IRQ(unsigned base)
  173. {
  174.   // returns: -1 if no intlevel found, or intlevel 0-15
  175.   char ier,mcr,imrm,imrs,maskm,masks,irqm,irqs;
  176.  
  177.   _asm cli;            // disable all CPU interrupts
  178.   ier = inp(base+1);   // read IER
  179.   outp(base+1,0);      // disable all UART ints
  180.   while (!(inp(base+5)&0x20));  // wait for the THR to be empty
  181.   mcr = inp(base+4);   // read MCR
  182.   outp(base+4,0x0F);   // connect UART to irq line
  183.   imrm = inp(0x21);    // read contents of master ICU mask register
  184.   imrs = inp(0xA1);    // read contents of slave ICU mask register
  185.   outp(0xA0,0x0A);     // next read access to 0xA0 reads out IRR
  186.   outp(0x20,0x0A);     // next read access to 0x20 reads out IRR
  187.   outp(base+1,2);      // let's generate interrupts...
  188.   maskm = inp(0x20);   // this clears all bits except for the one
  189.   masks = inp(0xA0);   // that corresponds to the int
  190.   outp(base+1,0);      // drop the int line
  191.   maskm &= ~inp(0x20); // this clears all bits except for the one
  192.   masks &= ~inp(0xA0); // that corresponds to the int
  193.   outp(base+1,2);      // and raise it again just to be sure...
  194.   maskm &= inp(0x20);  // this clears all bits except for the one
  195.   masks &= inp(0xA0);  // that corresponds to the int
  196.   outp(0xA1,~masks);   // now let us unmask this interrupt only
  197.   outp(0x21,~maskm);
  198.   outp(0xA0,0x0C);     // enter polled mode; Mike Surikov reported
  199.   outp(0x20,0x0C);     // that order is important with Pentium/PCI systems
  200.   irqs = inp(0xA0);    // and accept the interrupt
  201.   irqm = inp(0x20);
  202.   inp(base+2);         // reset transmitter interrupt in UART
  203.   outp(base+4,mcr);    // restore old value of MCR
  204.   outp(base+1,ier);    // restore old value of IER
  205.   if (masks) outp(0xA0,0x20);  // send an EOI to slave
  206.   if (maskm) outp(0x20,0x20);  // send an EOI to master
  207.   outp(0x21,imrm);     // restore old mask register contents
  208.   outp(0xA1,imrs);
  209.   _asm sti;
  210.   if (irqs&0x80)       // slave interrupt occured
  211.     return (irqs&0x07)+8;
  212.   if (irqm&0x80)       // master interrupt occured
  213.     return irqm&0x07;
  214.   return -1;
  215. }
  216.  
  217.  
  218.  
  219. Now the non-interrupt version of TX and RX.
  220.  
  221. Let's assume the following constants are set correctly (either by
  222. 'CONSTANT EQU value' or by '#define CONSTANT value'). You can easily use
  223. variables instead, but I wanted to save the extra lines for the ADD
  224. commands then necessary... A cute trick for calculating I/O addresses in
  225. assembly programs is this: load an index register (BX, BP, SI, or DI)
  226. with the base address (and keep it there), then use LEA DX,[BX+offset]
  227. before each IN/OUT instead of MOV DX,base; ADD DX,offset. It saves you
  228. one or two cycles. :)
  229.  
  230.   UART_BASEADDR   the base address of the UART
  231.   UART_BAUDRATE   the divisor value (eg. 12 for 9600 bps)
  232.   UART_LCRVAL     the value to be written to the LCR (eg. 0x1b for 8e1)
  233.   UART_FCRVAL     the value to be written to the FCR. Bit 0, 1 and 2 set,
  234.                   bits 6 & 7 according to trigger level wished (see above).
  235.                   0x87 is a good value, 0x7 establishes compatibility
  236.                   (except that there are some bits to be masked in the IIR).
  237.  
  238. First thing to do is initializing the UART. This works as follows:
  239.  
  240. UART_init proc near
  241.   push ax  ; we are 'clean guys'
  242.   push dx
  243.   mov  dx,UART_BASEADDR+3  ; LCR
  244.   mov  al,80h  ; set DLAB
  245.   out  dx,al
  246.   mov  dx,UART_BASEADDR    ; divisor
  247.   mov  ax,UART_BAUDRATE
  248.   out  dx,ax
  249.   mov  dx,UART_BASEADDR+3  ; LCR
  250.   mov  al,UART_LCRVAL  ; params
  251.   out  dx,al
  252.   mov  dx,UART_BASEADDR+4  ; MCR
  253.   xor  ax,ax  ; clear loopback
  254.   out  dx,al
  255.   ;***
  256.   pop  dx
  257.   pop  ax
  258.   ret
  259. UART_init endp
  260.  
  261. void UART_init()
  262. {
  263.    outp(UART_BASEADDR+3,0x80);
  264.    outpw(UART_BASEADDR,UART_BAUDRATE);
  265.    outp(UART_BASEADDR+3,UART_LCRVAL);
  266.    outp(UART_BASEADDR+4,0);
  267.    //***
  268. }
  269.  
  270. If we wanted to use the FIFO functions of the 16550A, we'd have to add
  271. some lines to the routines above (where the ***s are).
  272. In assembler:
  273.   mov  dx,UART_BASEADDR+2  ; FCR
  274.   mov  al,UART_FCRVAL
  275.   out  dx,al
  276. And in C:
  277.    outp(UART_BASEADDR+2,UART_FCRVAL);
  278.  
  279. Don't forget to disable the FIFO when your program exits! Some other
  280. software may rely on this!
  281.  
  282. Not very complex so far, isn't it? Well, I told you so at the very
  283. beginning, and I wanted to start easy. Now let's send a character.
  284.  
  285. UART_send proc near
  286.   ; character to be sent in AL
  287.   push dx
  288.   push ax
  289.   mov  dx,UART_BASEADDR+5
  290. us_wait:
  291.   in   al,dx  ; wait until we are allowed to write a byte to the THR
  292.   test al,20h
  293.   jz   us_wait
  294.   pop  ax
  295.   mov  dx,UART_BASEADDR
  296.   out  dx,al  ; then write the byte
  297.   pop  dx
  298.   ret
  299. UART_send endp
  300.  
  301. void UART_send(char character)
  302. {
  303.    while ((inp(UART_BASEADDR+5)&0x20)==0);
  304.    outp(UART_BASEADDR,(int)character);
  305. }
  306.  
  307. This one sends a null-terminated string.
  308.  
  309. UART_send_string proc near
  310.   ; DS:SI contains a pointer to the string to be sent.
  311.   push si
  312.   push ax
  313.   push dx
  314.   cld  ; we want to read the string in its correct order
  315. uss_loop:
  316.   lodsb
  317.   or   al,al  ; last character sent?
  318.   jz   uss_end
  319.   ;*1*
  320.   mov  dx,UART_BASEADDR+5
  321.   push ax
  322. uss_wait:
  323.   in   al,dx
  324.   test al,20h
  325.   jz   uss_wait
  326.   mov  dx,UART_BASEADDR
  327.   pop  ax
  328.   out  dx,al
  329.   ;*2*
  330.   jmp  uss_loop
  331. uss_end:
  332.   pop  dx
  333.   pop  ax
  334.   pop  si
  335.   ret
  336. UART_send_string endp
  337.  
  338. void UART_send_string(char *string)
  339. {
  340.    int i;
  341.    for (i=0; string[i]!=0; i++)
  342.       {
  343.       //*1*
  344.       while ((inp(UART_BASEADDR+5)&0x20)==0);
  345.       outp(UART_BASEADDR,(int)string[i]);
  346.       //*2*
  347.       }
  348. }
  349.  
  350. Of course we could have used our already programmed function/procedure
  351. UART_send instead of the piece of code limited by *1* and *2*, but we are
  352. interested in high-speed code and thus save the call/ret.
  353.  
  354. It shouldn't be a hard nut for you to modify the above function/procedure
  355. so that it sends a block of data rather than a null-terminated string. I'll
  356. omit that here.
  357.  
  358. Note that all these routines don't make any use of the TX FIFO! If we know
  359. for sure that it's a 16550A we're dealing with, and that its FIFOs are
  360. enabled, we could as well write up to 16 characters whenever bit 5 (THRE)
  361. of the LSR goes 1.
  362.  
  363. Now for reception. We want to program routines that do the following:
  364.   - check if a character has been received or an error occured
  365.   - read a character if there's one available
  366.  
  367. Both the C and the assembler routines return 0 (in AX) if there is
  368. neither an error condition nor a character available. If a character is
  369. available, Bit 8 is set and AL or the lower byte of the return value
  370. contains the character. Bit 9 is set if we lost data (overrun), bit 10
  371. signals a parity error, bit 11 signals a framing error, bit 12 shows if
  372. there is a break in the data stream and bit 15 signals if there are any
  373. errors in the FIFO (if we turned it on). The procedure/function is much
  374. smaller than this paragraph:
  375.  
  376. UART_get_char proc near
  377.   push dx
  378.   mov  dx,UART_BASEADDR+5
  379.   in   al,dx
  380.   xchg al,ah
  381.   and  ax,9f00h
  382.   test al,1
  383.   jz   ugc_nochar
  384.   mov  dx,UART_BASEADDR
  385.   in   al,dx
  386. ugc_nochar:
  387.   pop  dx
  388.   ret
  389. UART_get_char endp
  390.  
  391. unsigned UART_get_char()
  392. {
  393.    unsigned x;
  394.    x = (inp(UART_BASEADDR+5) & 0x9f) << 8;
  395.    if (x&0x100) x|=((unsigned)inp(UART_BASEADDR))&0xff;
  396.    return x;
  397. }
  398.  
  399. This procedure/function lets us easily keep track of what's happening
  400. with the RxD pin. It does not provide any information on the modem status
  401. lines! We'll program that later on.
  402.  
  403. If we wanted to show what's happening with the RxD pin, we'd just have to
  404. write a routine like the following (I use a macro in the assembler version
  405. to shorten the source code):
  406.  
  407. DOS_print macro pointer
  408.   ; prints a string in the code segment
  409.   push ax
  410.   push ds
  411.   push dx
  412.   push cs
  413.   pop  ds
  414.   mov  dx,pointer
  415.   mov  ah,9
  416.   int  21h
  417.   pop  dx
  418.   pop  ds
  419.   pop  ax
  420. endm
  421.  
  422. UART_watch_rxd proc near
  423. uwr_loop:
  424.   ; check if keyboard hit; we want a possibility to break the loop
  425.   mov  ah,1  ; Beware! Don't call INT 16h with high transmission
  426.   int  16h   ; rates, it won't work!
  427.   jnz  uwr_exit
  428.   call UART_get_char
  429.   or   ax,ax
  430.   jz   uwr_loop
  431.   test ah,1  ; is there a character in AL?
  432.   jz   uwr_nodata
  433.   push ax    ; yes, print it
  434.   mov  dl,al ;\
  435.   mov  ah,2  ; better use this for high rates: mov ah,0eh
  436.   int  21h   ;/                                int 10h
  437.   pop  ax
  438. uwr_nodata:
  439.   test ah,0eh ; any error at all?
  440.   jz   uwr_loop  ; this speeds up things since errors should be rare
  441.   test ah,2  ; overrun error?
  442.   jz   uwr_noover
  443.   DOS_print overrun_text
  444. uwr_noover:
  445.   test ah,4  ; parity error?
  446.   jz   uwr_nopar
  447.   DOS_print parity_text
  448. uwr_nopar:
  449.   test ah,8  ; framing error?
  450.   jz   uwr_loop
  451.   DOS_print framing_text
  452.   jmp  uwr_loop
  453. uwr_exit:
  454.   ret
  455. overrun_text    db "*** Overrun Error ***$"
  456. parity_text     db "*** Parity Error ***$"
  457. framing_text    db "*** Framing Error ***$"
  458. UART_watch_rxd endp
  459.  
  460. void UART_watch_rxd()
  461. {
  462.    union {
  463.       unsigned val;
  464.       char character;
  465.       } x;
  466.    while (!kbhit()) {
  467.       x.val=UART_get_char();
  468.       if (!x.val) continue;  // nothing? Continue
  469.       if (x.val&0x100) putc(x.character);  // character? Print it
  470.       if (!(x.val&0xe00)) continue;  // any error condidion? No, continue
  471.       if (x.val&0x200) printf("*** Overrun Error ***");
  472.       if (x.val&0x400) printf("*** Parity Error ***");
  473.       if (x.val&0x800) printf("*** Framing Error ***");
  474.       }
  475. }
  476.  
  477. The RX routines make use of the RX FIFO without any additional programming.
  478.  
  479. If you call these routines from a function/procedure as shown below,
  480. you've got a small terminal program!
  481.  
  482. terminal proc near
  483. ter_loop:
  484.   call UART_watch_rxd  ; watch line until a key is pressed
  485.   xor  ax,ax  ; get that key from the keyboard buffer
  486.   int  16h
  487.   cmp  al,27  ; is it ESC?
  488.   jz   ter_end  ; yes, then end this function
  489.   call UART_send  ; send the character typed if it's not ESC
  490.   jmp  ter_loop  ; don't forget to check if data comes in
  491. ter_end:
  492.   ret
  493. terminal endp
  494.  
  495. void terminal()
  496. {
  497.    int key;
  498.    while (1)
  499.       {
  500.       UART_watch_rxd();
  501.       key=getche();
  502.       if (key==27) break;
  503.       UART_send((char)key);
  504.       }
  505. }
  506.  
  507. These, of course, should be called from an embedding routine like the
  508. following (the assembler routines concatenated will assemble as an .EXE
  509. file. Put the lines 'code segment' and 'assume cs:code,ss:stack' to the
  510. front).
  511.  
  512. main proc near
  513.   call UART_init
  514.   call terminal
  515.   mov  ax,4c00h
  516.   int  21h
  517. main endp
  518. code ends
  519. stack segment stack 'stack'
  520.   dw 128 dup (?)
  521. stack ends
  522. end main
  523.  
  524. void main()
  525. {
  526.    UART_init();
  527.    terminal();
  528. }
  529.  
  530. Here we are. Now you've got everything you need to program simple
  531. polling UART software.
  532.  
  533. You know the way. Go and add functions to check if a data set is there,
  534. then establish a connection. Don't know how? Set DTR, wait for DSR.
  535. If you want to send, set RTS and wait for CTS before you actually transmit
  536. data. You don't need to store old values of the MCR: this register is
  537. readable. Just read in the data, AND/OR the bits as required and write the
  538. byte back.
  539.  
  540.  
  541. Let us now write the interrupt-driven versions of the routines. This is going
  542. to be a bit voluminous, so I draw the scene and leave the painting to you. If
  543. you want to implement interrupt-driven routines in a C program use either the
  544. inline-assembler feature or link the objects together. Of course you can also
  545. program interrupts in C (or other languages for that matter (are there
  546. any? :)).
  547.  
  548. You'll find a complete program using interrupts at the end of this chapter.
  549.  
  550. First thing to do is initialize the UART the same way as shown above.
  551. But there is some more work to be done before you enable the UART
  552. interrupt: FIRST SET THE INTERRUPT VECTOR CORRECTLY! Use function 25h of
  553. the DOS interrupt 21h. Remember to store the old value (obtained by calling
  554. DOS interrupt 21h function 35h) and to restore this value when exiting
  555. to DOS again. See also the note on known bugs if you've got a 8250.
  556.  
  557. UART_INT      EQU 0Ch  ; for COM2 / COM4 use 0bh
  558. UART_ONMASK   EQU 11101111b  ; for COM2 / COM4 use 11110111b
  559. UART_OFFMASK  EQU NOT UART_ONMASK
  560. UART_IERVAL   EQU ?   ; replace ? by any value between 0h and 0fh
  561.                       ; (dependent on which ints you want)
  562.                       ; DON'T SET bit 1 now! (not with this kind of service
  563.                       ; routine, that is)
  564. UART_OLDVEC   DD  ?
  565.  
  566. initialize_UART_interrupt proc near
  567.   push ds
  568.   push es  ; first thing is to store the old interrupt
  569.   push bx  ; vector
  570.   mov  ax,3500h+UART_INT
  571.   int  21h
  572.   mov  word ptr UART_OLDVEC,bx
  573.   mov  word ptr UART_OLDVEC+2,es
  574.   pop  bx
  575.   pop  es
  576.   push cs  ; build a pointer in DS:DX
  577.   pop  ds
  578.   lea  dx,interrupt_service_routine
  579.   mov  ax,2500h+UART_INT
  580.   int  21h ; and ask DOS to set this pointer as the new interrrupt vector
  581.   pop  ds
  582.   mov  dx,UART_BASEADDR+4  ; MCR
  583.   in   al,dx
  584.   or   al,8  ; set OUT2 bit to enable interrupts
  585.   out  dx,al
  586.   mov  dx,UART_BASEADDR+1  ; IER
  587.   mov  al,UART_IERVAL  ; enable the interrupts we want
  588.   out  dx,al
  589.   in   al,21h  ; last thing to do is unmask the int in the ICU
  590.   and  al,UART_ONMASK
  591.   out  21h,al
  592.   sti  ; and free interrupts if they have been disabled
  593.   ret
  594. initialize_UART_interrupt endp
  595.  
  596. deinitialize_UART_interrupt proc near
  597.   push ds
  598.   lds  dx,UART_OLDVEC
  599.   mov  ax,2500h+UART_INT
  600.   int  21h
  601.   pop  ds
  602.   in   al,21h  ; mask the UART interrupt
  603.   or   al,UART_OFFMASK
  604.   out  21h,al
  605.   mov  dx,UART_BASEADDR+1
  606.   xor  al,al
  607.   out  dx,al   ; clear all interrupt enable bits
  608.   mov  dx,UART_BASEADDR+4
  609.   out  dx,al   ; and disconnect the UART from the ICU
  610.   ret
  611. deinitialize_UART_interrupt endp
  612.  
  613. Now the interrupt service routine. It has to follow several rules:
  614. first, it MUST NOT change the contents of any register of the CPU! Then it
  615. has to tell the ICU (did I tell you that this is the interrupt control
  616. unit? It is also called PIC Programmable Interrupt Controller) that the
  617. interrupt is being serviced. Next thing is test which part of the UART needs
  618. service. Let's have a look at the following procedure:
  619.  
  620. interupt_service_routine proc far  ; define as near if you want to link .COM
  621.   ;*1*                             ; it doesn't matter anyway since IRET is
  622.   push ax                          ; always a FAR command
  623.   push cx
  624.   push dx
  625.   push bx
  626.   push sp
  627.   push bp
  628.   push si
  629.   push di
  630.   ;*2*   replace the part between *1* and *2* by pusha on an 80186+ system
  631.   push ds
  632.   push es
  633.   in   al,21h
  634.   or   al,UART_OFFMASK
  635.   out  21h,al
  636.   mov  al,20h    ; remember: first thing to do in interrupt routines is tell
  637.   out  20h,al    ; the ICU about the service being done. This avoids lock-up
  638. int_loop:
  639.   mov  dx,UART_BASEADDR+2  ; IIR
  640.   in   al,dx  ; check IIR info
  641.   test al,1
  642.   jnz  int_end
  643.   and  ax,6  ; we're interested in bit 1 & 2 (see data sheet info)
  644.   mov  si,ax ; this is already an index! Well-devised, huh?
  645.   call word ptr cs:int_servicetab[si]  ; ensure a near call is used...
  646.   jmp  int_loop
  647. int_end:
  648.   in   al,21h
  649.   and  al,UART_ONMASK
  650.   out  21h,al
  651.   pop  es
  652.   pop  ds
  653.   ;*3*
  654.   pop  di
  655.   pop  si
  656.   pop  bp
  657.   pop  sp
  658.   pop  bx
  659.   pop  dx
  660.   pop  cx
  661.   pop  ax
  662.   ;*4*   *3* - *4* can be replaced by popa on an 80186+ based system
  663.   iret
  664. interupt_service_routine endp
  665.  
  666. This is the part of the service routine that does the decisions. Now we
  667. need four different service routines to cover all four interrupt source
  668. possibilities (EVEN IF WE DIDN'T ENABLE THEM! Let's play this safe).
  669.  
  670. int_servicetab    DW int_modem, int_tx, int_rx, int_status
  671.  
  672. int_modem proc near
  673.   mov  dx,UART_BASE+6  ; MSR
  674.   in   al,dx
  675.   ; do with the info what you like; probably just ignore it...
  676.   ; but YOU MUST READ THE MSR or you'll lock up the interrupt!
  677.   ret
  678. int_modem endp
  679.  
  680. int_tx proc near
  681.   ; get next byte of data from a buffer or something
  682.   ; (remember to set the segment registers correctly!)
  683.   ; and write it to the THR (offset 0)
  684.   ; if no more data is to be sent, disable the THRE interrupt
  685.   ; If the FIFOs are switched on (and you've made sure it's a 16550A!), you
  686.   ; can write up to 16 characters
  687.  
  688.   ; end of data to be sent?
  689.   ; no, jump to end_int_tx
  690.   mov  dx,UART_BASEADDR+1
  691.   in   al,dx
  692.   and  al,00001101b
  693.   out  dx,al
  694. end_int_tx:
  695.   ret
  696. int_tx endp
  697.  
  698. int_rx proc near
  699.   mov  dx,UART_BASEADDR
  700.   in   al,dx
  701.   ; do with the character what you like (best write it to a
  702.   ; FIFO buffer [not the one of the 16550A, silly! :)])
  703.   ; the following lines speed up FIFO mode operation
  704.   mov  dx,UART_BASEADDR+5
  705.   in   al,dx
  706.   test al,1
  707.   jnz  int_rx
  708.   ; these lines are a cure for the well-known problem of TX interrupt
  709.   ; lock-ups when receiving and transmitting at the same time
  710.   test al,40h
  711.   je   dont_unlock
  712.   call int_tx
  713. dont_unlock:
  714.   ret
  715. int_rx endp
  716.  
  717. int_status proc near
  718.   mov  dx,UART_BASEADDR+5
  719.   in   al,dx
  720.   ; do what you like. It's just important to read the LSR
  721.   ret
  722. int_status endp
  723.  
  724. How is data sent now? Write it to a FIFO buffer (that's nothing to do with
  725. the built-in FIFOs of the 16550!) that is read by the interrupt routine.
  726. Then set bit 1 of the IER and check if this has already started transmission.
  727. If not, you'll have to start it by hand (just call the int_tx routine). THIS
  728. IS DUE TO THOSE NUTTY GUYS AT BIG BLUE WHO DECIDED TO USE EDGE TRIGGERED
  729. INTERRUPTS INSTEAD OF PROVIDING ONE SINGLE FLIP FLOP FOR THE 8253/8254!
  730. See the "Known Problems" section for another good method of handling the
  731. UART interrupts that avoids all these problems.
  732.  
  733. This procedure can be a C function, too. It is not time-critical at all.
  734.  
  735.   ; copy data to buffer
  736.  
  737.   mov  dx,UART_BASEADDR+1  ; IER
  738.   in   al,dx
  739.   or   al,2  ; set bit 1
  740.   out  dx,al
  741.   nop
  742.   nop  ; give the UART some time to kick the interrupt...
  743.   nop
  744.   mov  dx,UART_BASEADDR+5  ; LSR
  745.   cli  ; make sure no interrupts get in-between if not already running
  746.   in   al,dx
  747.   test al,40h  ; is there a transmission running?
  748.   jz   dont_crank  ; yes, so don't mess it up
  749.   call int_tx  ; no, crank it up
  750.   sti
  751. dont_crank:
  752.  
  753. Well, that's it! Your main program has to take care of the buffers,
  754. nothing else!
  755.  
  756. Remember to call deinitialize_UART_interrupt before exiting to DOS! In C,
  757. this can easily be done by adding the function to the at-exit list with
  758. the atexit() function. You won't have to worry about the myriads of ways
  759. your program could terminate then.
  760.  
  761. For those of you who prefer learning by watching rather than learning by
  762. doing ("lazy" is such an ignorant word :-), here's the source of a
  763. small terminal program. It can be assembled with TASM or ML without
  764. any change. Wire together two PCs (three-wire-connection, see the
  765. beginning of this file) and start it on each of them. You can then
  766. type messages on both keyboards that can be viewed on both screens.
  767. If you press F1, a large string is being sent (but not displayed on
  768. the sender's screen). Ctrl-X terminates the program.
  769.  
  770.  
  771. ----8<--------8<--------8<--------8<--------8<--------8<--------8<----
  772.  
  773.   ; just a small terminal program using interrupts.
  774.   ; It's quite dumb: it uses the BIOS for screen output
  775.   ; and keyboard input
  776.   ; assemble and link as .EXE (just type ml name)
  777.   ; If you have a 16550 (not a 16550A), you may lose
  778.   ; characters since the fifos are turned on (see "Known problems
  779.   ; with several chips")
  780.   ; If your BIOS locks the interrupts while scrolling (some do),
  781.   ; you may encounter data loss at high rates.
  782.  
  783. model small
  784. dosseg
  785.  
  786. INTNUM      equ 0Ch          ; COM1; COM2: 0Bh
  787. OFFMASK     equ 00010000b    ; COM1; COM2: 00001000b
  788. ONMASK      equ not OFFMASK
  789. UART_BASE   equ 3F8h         ; COM1; COM2: 2F8h
  790. UART_RATE   equ 12           ; 9600 bps, see table in this file
  791. UART_PARAMS equ 00000011b    ; 8n1, see tables
  792. RXFIFOSIZE  equ 8096         ; set this to your needs
  793. TXFIFOSIZE  equ 8096         ; dito.
  794.                              ; the fifos must be large on slow computers
  795.                              ; and can be small on fast ones
  796.                              ; These have nothing to do with the 16550A's
  797.                              ; built-in FIFOs!
  798.  
  799. .data
  800. long_text  db  0dh
  801.     db  "This is a very long test string. It serves the purpose of",0dh
  802.     db  "demonstrating that our interrupt-driven routines are capable",0dh
  803.     db  "of coping with pressure situations like the one we provoke",0dh
  804.     db  "by sending large bunches of characters in each direction at",0dh
  805.     db  "the same time. Run this test by pressing F1 at a low data",0dh
  806.     db  "rate and a high data rate to see why serial transmission and",0dh
  807.     db  "reception should be programmed interrupt-driven. You won't lose",0dh
  808.     db  "a single character as long as you don't overload the fifos, no",0dh
  809.     db  "matter how hard you try!",0dh,0
  810.  
  811. ds_dgroup  macro
  812.   mov  ax,DGROUP
  813.   mov  ds,ax
  814.   assume  ds:DGROUP
  815. endm
  816.  
  817. ds_text  macro
  818.   push  cs
  819.   pop   ds
  820.   assume  ds:_TEXT
  821. endm
  822.  
  823. rx_checkwrap  macro
  824.   local rx_nowrap
  825.   cmp  si,offset rxfifo+RXFIFOSIZE
  826.   jb  rx_nowrap
  827.   lea  si,rxfifo
  828. rx_nowrap:
  829. endm
  830.  
  831. tx_checkwrap  macro
  832.   local tx_nowrap
  833.   cmp  si,offset txfifo+TXFIFOSIZE
  834.   jb  tx_nowrap
  835.   lea  si,txfifo
  836. tx_nowrap:
  837. endm
  838.  
  839. .stack 256
  840.  
  841. .data?
  842. old_intptr  dd  ?
  843. rxhead      dw  ?
  844. rxtail      dw  ?
  845. txhead      dw  ?
  846. txtail      dw  ?
  847. bitxfifo    dw  1  ; size of built-in TX fifo (1 if no fifo)
  848. rxfifo      db  RXFIFOSIZE dup (?)
  849. txfifo      db  TXFIFOSIZE dup (?)
  850.  
  851. .code
  852. start  proc far
  853.   call  install_interrupt_handler
  854.   call  clear_fifos
  855.   call  clear_screen
  856.   call  init_UART
  857. continue:
  858.   call  read_RX_fifo
  859.   call  read_keyboard
  860.   jnc  continue
  861.   call  clean_up
  862.   mov  ax,4c00h
  863.   int  21h  ; return to DOS
  864. start  endp
  865.  
  866. interrupt_handler  proc far
  867.   assume  ds:nothing,es:nothing,ss:nothing,cs:_text
  868.   push  ax
  869.   push  cx
  870.   push  dx  ; first save the regs we need to change
  871.   push  ds
  872.   push  si
  873.   in  al,21h
  874.   or  al,OFFMASK   ; disarm the interrupt
  875.   out 21h,al
  876.   mov  al,20h  ; acknowledge interrupt
  877.   out  20h,al
  878.  
  879. ih_continue:
  880.   mov  dx,UART_BASE+2
  881.   xor  ax,ax
  882.   in  al,dx  ; get interrupt cause
  883.   test  al,1  ; did the UART generate the int?
  884.   jne  ih_sep  ; no, then it's somebody else's problem
  885.   and  al,6  ; mask bits not needed
  886.   mov  si,ax  ; make a pointer out of it
  887.   call  interrupt_table[si]  ; serve this int
  888.   jmp  ih_continue  ; and look for more things to be done
  889. ih_sep:
  890.  
  891.   in  al,21h
  892.   and al,ONMASK  ; rearm the interrupt
  893.   out 21h,al
  894.  
  895.   pop  si
  896.   pop  ds
  897.   pop  dx  ; restore regs
  898.   pop  cx
  899.   pop  ax
  900.   iret
  901. interrupt_table  dw  int_modem,int_tx,int_rx,int_status
  902. interrupt_handler  endp
  903.  
  904. int_modem  proc near
  905.   ; just clear modem status, we are not interested in it
  906.   mov  dx,UART_BASE+6
  907.   in  al,dx
  908.   ret
  909. int_modem  endp
  910.  
  911. int_tx  proc near
  912.   ds_dgroup
  913.   ; check if there's something to be sent
  914.   mov  si,txtail
  915.   mov  cx,bitxfifo
  916. itx_more:
  917.   cmp  si,txhead
  918.   je  itx_nothing
  919.   cld
  920.   lodsb
  921.   mov  dx,UART_BASE
  922.   out  dx,al  ; write it to the THR
  923.   ; check for wrap-around in our fifo
  924.   tx_checkwrap
  925.   ; send as much bytes as the chip can take when available
  926.   loop itx_more
  927.   jmp  itx_dontstop
  928. itx_nothing:
  929.   ; no more data in the fifo, so inhibit TX interrupts
  930.   mov  dx,UART_BASE+1
  931.   mov  al,00000001b
  932.   out  dx,al
  933. itx_dontstop:
  934.   mov  txtail,si
  935.   ret
  936. int_tx  endp
  937.  
  938. int_rx  proc near
  939.   ds_dgroup
  940.   mov  si,rxhead
  941. irx_more:
  942.   mov  dx,UART_BASE
  943.   in  al,dx
  944.   mov  byte ptr [si],al
  945.   inc  si
  946.   ; check for wrap-around
  947.   rx_checkwrap
  948.   ; see if there are more bytes to be read
  949.   mov  dx,UART_BASE+5
  950.   in  al,dx
  951.   test  al,1
  952.   jne  irx_more
  953.   mov  rxhead,si
  954.   test  al,40h  ; Sometimes when sending and receiving at the
  955.   jne  int_tx   ; same time, TX ints get lost. This is a cure.
  956.   ret
  957. int_rx  endp
  958.  
  959. int_status  proc near
  960.   ; just clear the status ("this trivial task is left as an exercise
  961.   ; to the student")
  962.   mov  dx,UART_BASE+5
  963.   in  al,dx
  964.   ret
  965. int_status  endp
  966.  
  967. read_RX_fifo  proc near
  968.   ; see if there are bytes to be read from the fifo
  969.   ; we read a maximum of 16 bytes, then return in order
  970.   ; not to break keyboard control
  971.   ds_dgroup
  972.   cld
  973.   mov  cx,16
  974.   mov  si,rxtail
  975. rx_more:
  976.   cmp  si,rxhead
  977.   je  rx_nodata
  978.   lodsb
  979.   call  output_char
  980.   ; check for wrap-around
  981.   rx_checkwrap
  982.   loop  rx_more
  983. rx_nodata:
  984.   mov  rxtail,si
  985.   ret
  986. read_RX_fifo  endp
  987.  
  988. read_keyboard  proc near
  989.   ds_dgroup
  990.   ; check for keys pressed
  991.   mov  ah,1
  992.   int  16h
  993.   je  rk_nokey
  994.   xor  ax,ax
  995.   int  16h
  996.   cmp  ax,2d18h  ; is it Ctrl-X?
  997.   stc
  998.   je  rk_ctrlx
  999.   cmp  ax,3b00h  ; is it F1?
  1000.   jne  rk_nf1
  1001.   lea  si,long_text  ; send a very long test string
  1002.   call  send_string
  1003.   jmp  rk_nokey
  1004. rk_nf1:
  1005.   ; echo the character to the screen
  1006.   call  output_char
  1007.  
  1008.   call  send_char
  1009. rk_nokey:
  1010.   clc
  1011. rk_ctrlx:
  1012.   ret
  1013. read_keyboard  endp
  1014.  
  1015.  
  1016. install_interrupt_handler  proc near
  1017.   ds_dgroup
  1018.   ; install interrupt handler first
  1019.   mov  ax,3500h+INTNUM
  1020.   int  21h
  1021.   mov  word ptr old_intptr,bx
  1022.   mov  word ptr old_intptr+2,es
  1023.   mov  ax,2500h+INTNUM
  1024.   ds_text
  1025.   lea  dx,interrupt_handler
  1026.   int  21h
  1027.   ret
  1028. install_interrupt_handler  endp
  1029.  
  1030. clear_fifos  proc near
  1031.   ds_dgroup
  1032.   ; clear fifos (not those in the 16550A, but ours)
  1033.   lea  ax,rxfifo
  1034.   mov  rxhead,ax
  1035.   mov  rxtail,ax
  1036.   lea  ax,txfifo
  1037.   mov  txhead,ax
  1038.   mov  txtail,ax
  1039.   ret
  1040. clear_fifos  endp
  1041.  
  1042. init_UART  proc near
  1043.   ; initialize the UART
  1044.   mov  dx,UART_BASE+3
  1045.   mov  al,80h
  1046.   out  dx,al  ; make DL register accessible
  1047.   mov  dx,UART_BASE
  1048.   mov  ax,UART_RATE
  1049.   out  dx,ax  ; write bps rate divisor
  1050.   mov  dx,UART_BASE+3
  1051.   mov  al,UART_PARAMS
  1052.   out  dx,al  ; write parameters
  1053.   
  1054.   ; is it a 16550A?
  1055.   mov  dx,UART_BASE+2
  1056.   in   al,dx
  1057.   and  al,11000000b
  1058.   cmp  al,11000000b
  1059.   jne  iu_nofifos
  1060.   mov  bitxfifo,16
  1061.   mov  dx,UART_BASE+2
  1062.   mov  al,11000111b
  1063.   out  dx,al  ; clear and enable the fifos if they exist
  1064. iu_nofifos:
  1065.   mov  dx,UART_BASE+1
  1066.   mov  al,00000001b  ; allow RX interrupts
  1067.   out  dx,al
  1068.   mov  dx,UART_BASE
  1069.   in  al,dx  ; clear receiver
  1070.   mov  dx,UART_BASE+5
  1071.   in  al,dx  ; clear line status
  1072.   inc  dx
  1073.   in  al,dx  ; clear modem status
  1074.   ; free interrupt in the ICU
  1075.   in  al,21h
  1076.   and  al,ONMASK
  1077.   out  21h,al
  1078.   ; and enable ints from the UART
  1079.   mov  dx,UART_BASE+4
  1080.   mov  al,00001000b
  1081.   out  dx,al
  1082.   ret
  1083. init_UART  endp
  1084.  
  1085. clear_screen  proc near
  1086.   mov  ah,0fh  ; allow all kinds of video adapters to be used
  1087.   int  10h
  1088.   cmp  al,7
  1089.   je  cs_1
  1090.   mov  al,3
  1091. cs_1:
  1092.   xor  ah,ah
  1093.   int  10h
  1094.   ret
  1095. clear_screen  endp
  1096.  
  1097. clean_up  proc near
  1098.   ds_dgroup
  1099.   ; lock int in the ICU
  1100.   in  al,21h
  1101.   or  al,OFFMASK
  1102.   out  21h,al
  1103.   xor  ax,ax
  1104.   mov  dx,UART_BASE+4  ; disconnect the UART from the int line
  1105.   out  dx,al
  1106.   mov  dx,UART_BASE+1  ; disable UART ints
  1107.   out  dx,al
  1108.   mov  dx,UART_BASE+2  ; disable the fifos (old software relies on it)
  1109.   out  dx,al
  1110.   ; restore int vector
  1111.   lds  dx,old_intptr
  1112.   mov  ax,2500h+INTNUM
  1113.   int  21h
  1114.   ret
  1115. clean_up  endp
  1116.  
  1117. output_char  proc near
  1118.   push  si
  1119.   push  ax
  1120. oc_cr:
  1121.   push  ax
  1122.   mov  ah,0eh  ; output character using BIOS TTY
  1123.   int  10h     ; it's your task to improve this
  1124.   pop  ax
  1125.   cmp  al,0dh  ; add LF after CR; change it if you don't like it
  1126.   mov  al,0ah
  1127.   je  oc_cr
  1128.   pop  ax
  1129.   pop  si
  1130.   ret
  1131. output_char  endp
  1132.  
  1133. send_char proc near
  1134.   push  si
  1135.   push  ax
  1136.   ds_dgroup
  1137.   pop  ax
  1138.   mov  si,txhead
  1139.   mov  byte ptr [si],al
  1140.   inc  si
  1141.   ; check for wrap-around
  1142.   tx_checkwrap
  1143.   mov  txhead,si
  1144.   ; test if the interrupt is running at the moment
  1145.   mov  dx,UART_BASE+5
  1146.   in  al,dx
  1147.   test  al,40h
  1148.   je  sc_dontcrank
  1149.   ; crank it up
  1150.   ; note that this might not work with some very old 8250s
  1151.   mov  dx,UART_BASE+1
  1152.   mov  al,00000011b
  1153.   out  dx,al
  1154. sc_dontcrank:
  1155.   pop  si
  1156.   ret
  1157. send_char  endp
  1158.  
  1159. send_string  proc near
  1160.   ; sends a null-terminated string pointed at by DS:SI
  1161.   ds_dgroup
  1162.   cld
  1163. ss_more:
  1164.   lodsb
  1165.   or  al,al
  1166.   je  ss_end
  1167.   call send_char
  1168.   jmp  ss_more
  1169. ss_end:
  1170.   ret
  1171. send_string  endp
  1172.  
  1173. end start
  1174.  
  1175. ---->8-------->8-------->8-------->8-------->8-------->8-------->8----
  1176.  
  1177. Stephen Warner provided me with an assembly source of a TSR program that
  1178. puts every character it receives from the serial port in the keyboard
  1179. buffer. This allows to remotely control nearly every other program; it works
  1180. with ATs and higher computers only. I decided not to add it to this file
  1181. since it doesn't show anything about programming the serial port that's
  1182. not already covered by other listings in this file. If you are interested
  1183. in it, you can obtain it from the ftp archive (it is named
  1184. "The_Serial_Port.more01"). See the beginning of this file.
  1185.  
  1186. One more thing: always remember that at 115,200 bps there is service to
  1187. be done at least every 85 microseconds! On an XT with 4.77 MHz this is
  1188. about 40 assembler commands! So forget about servicing the serial port at
  1189. this rate in high-level languages on such computers. Using a 16550A is
  1190. strongly recommended at high rates (turn on FIFOs) but not necessary
  1191. with otherwise decent hardware.
  1192.  
  1193. The interrupt service routines can be accelerated by not pushing that
  1194. much registers, and pusha and popa are fast replacements for 8 other
  1195. pushs/pops.
  1196.  
  1197.  
  1198. Well, that's the end of my short :-) summary. Don't hesitate to correct
  1199. me if I'm wrong (preferably via email) in the details (I hope not, but it's
  1200. not easy to find typographical and other errors in a text that you've
  1201. written yourself). And please help me to complete this file! If you've got
  1202. anything to add, email it to me and I'll spread it round.
  1203.  
  1204. I've received a lot of feedback from you, and I'd like to thank everybody
  1205. who encouraged me to continue the work on this file.
  1206.  
  1207.  
  1208. Yours
  1209.  
  1210.       Chris
  1211.  
  1212. P.S. You surely have noticed that English isn't my native tongue... so please
  1213. excuse everything that's not pleasant for the eye, or, even better, tell me
  1214. about it! It shouldn't be an ordeal though, at least some have assured me
  1215. so...
  1216.  
  1217.  
  1218.  
  1219. --
  1220. Chris Blum <chris@phil.uni-sb.de>  http://www.phil.uni-sb.de/~chris/
  1221.  
  1222.